""" CheckBoxDelegateQt.py: Delegate for editing bool values via a checkbox with no label centered in its cell.
"""
from PyQt5.QtCore import Qt, QEvent, QPoint, QRect
from PyQt5.QtWidgets import QComboBox,QLineEdit, QStyledItemDelegate, QStyleOptionButton, QStyle, QApplication
from PyQt5 import QtWidgets,QtCore,QtGui
import traceback
import logging
import datetime
def is_float(a_string):    
    try:
        float(a_string)
        return True
    except ValueError:
        return False
    except TypeError:
        return False
class ComboBoxDelegateQt(QStyledItemDelegate):
    """ Delegate for editing a list of choices via a combobox.
    The choices attribute is a list of either values or (key, value) tuples.
    In the first case, the str rep of the values are directly displayed in the combobox.
    In the latter case, the str rep of only the keys are displayed in the combobox, and the values can be any object.
        Although only the keys are displayed in the view, the model data is set to the actual values, not the keys.
    Upon selection, view will display the str rep of either the value itself or its key if it exists.

    For example:
        To select from some integers, set choices = [1, 3, 10, 100].
            Combobox entries will be '1', '3', '10' and '100'.
            Upon selection model data will be set to the selected integer value and view will show the str rep of this value.
        To select from two of your custom objects, set choices = [('A', MyObject()), ('B', MyObject())]
            Combobox entries will be 'A' and 'B'.
            Upon selection model data will be set to the selected MyObject instance and view will show its key (either 'A' or 'B')..
    """
    def __init__(self, choices=None, parent=None):
        super().__init__(parent)
        self.choices = choices if (choices is not None and type(choices) is list) else []

    def createEditor(self, parent, option, index):
        """ Return QComboBox with list of choices (either values or their associated keys if they exist).
        """
        try:
            editor = QComboBox(parent)
            value = index.model().data(index, Qt.DisplayRole)
            for i, choice in enumerate(self.choices):
                if (type(choice) is tuple) and (len(choice) == 2):
                    # choice is a (key, value) tuple.
                    key, val = choice
                    editor.addItem(str(key))  # key MUST be representable as a str.
                    if val == value:
                        editor.setCurrentIndex(i)
                else:
                    # choice is a value.
                    editor.addItem(str(choice))  # choice MUST be representable as a str.
                    if choice == value:
                        editor.setCurrentIndex(i)
            return editor
        except:
            return None

    def setModelData(self, editor, model, index):
        """ Set model data to current choice (if choice is a key, set data to its associated value).
        """
        try:
            choice = self.choices[editor.currentIndex()]
            if (type(choice) is tuple) and (len(choice) == 2):
                # choice is a (key, value) tuple.
                key, val = choice
                value = copy.deepcopy(val)  # Deepcopy of val in case it is a complex object.
            else:
                # choice is a value.
                value = choice
            model.setData(index, value, Qt.EditRole)
            index.model().dataChanged.emit(index, index)  # Tell model to update cell display.
        except:
            traceback.print_exc()
            pass

    def displayText(self, value, locale):
        """ Show str rep of current choice (or choice key if choice is a (key, value) tuple).
        """
        try:
            for choice in self.choices:
                if (type(choice) is tuple) and (len(choice) == 2):
                    # choice is a (key, value) tuple.
                    # Display the key, not the value.
                    key, val = choice
                    if val == value:
                        return str(key)
                else:
                    # choice is a value.
                    # Display it's str rep.
                    if choice == value:
                        return str(choice)
            # If value is not in our list of choices, show str rep of value.
            return str(value)
        except:
            traceback.print_exc()
            return ""
class CheckBoxDelegateQt(QStyledItemDelegate):
    """ Delegate for editing bool values via a checkbox with no label centered in its cell.
    Does not actually create a QCheckBox, but instead overrides the paint() method to draw the checkbox directly.
    Mouse events are handled by the editorEvent() method which updates the model's bool value.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        """ Important, otherwise an editor is created if the user clicks in this cell.
        """
        return None

    def paint(self, painter, option, index):
        """ Paint a checkbox without the label.
        """
        checked = bool(index.model().data(index, Qt.DisplayRole))
        opts = QStyleOptionButton()
        opts.state |= QStyle.State_Active
        if index.flags() & Qt.ItemIsEditable:
            opts.state |= QStyle.State_Enabled
        else:
            opts.state |= QStyle.State_ReadOnly
        if checked:
            opts.state |= QStyle.State_On
        else:
            opts.state |= QStyle.State_Off
        opts.rect = self.getCheckBoxRect(option)
        QApplication.style().drawControl(QStyle.CE_CheckBox, opts, painter)

    def editorEvent(self, event, model, option, index):
        """ Change the data in the model and the state of the checkbox if the
        user presses the left mouse button and this cell is editable. Otherwise do nothing.
        """
        if not (index.flags() & Qt.ItemIsEditable):
            return False
        if event.button() == Qt.LeftButton:
            if event.type() == QEvent.MouseButtonRelease:
                if self.getCheckBoxRect(option).contains(event.pos()):
                    self.setModelData(None, model, index)
                    return True
            elif event.type() == QEvent.MouseButtonDblClick:
                if self.getCheckBoxRect(option).contains(event.pos()):
                    return True
        return False

    def setModelData(self, editor, model, index):
        """ Toggle the boolean state in the model.
        """
        checked = not bool(index.model().data(index, Qt.DisplayRole))
        model.setData(index, checked, Qt.EditRole)

    def getCheckBoxRect(self, option):
        """ Get rect for checkbox centered in option.rect.
        """
        # Get size of a standard checkbox.
        opts = QStyleOptionButton()
        checkBoxRect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator, opts, None)
        # Center checkbox in option.rect.
        x = option.rect.x()
        y = option.rect.y()
        w = option.rect.width()
        h = option.rect.height()
        checkBoxTopLeftCorner = QPoint(int(x + w / 2 - checkBoxRect.width() / 2),int(y + h / 2 - checkBoxRect.height() / 2))
        return QRect(checkBoxTopLeftCorner, checkBoxRect.size())
class DoubleViewDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self,myformat,parent=None):
        super().__init__(parent)
        self.myformat=myformat
    def createEditor(self, parent, option, index):
        """ Return QComboBox with list of choices (either values or their associated keys if they exist).
        """
        return None

    def paint(self, painter, option, index):
        value = index.model().data(index)
        if is_float(value):
            option.text = self.myformat % float(value)
        else:
            option.text = value
        widget = option.widget
        if widget:
            style = widget.style()
        else:
            style = QApplication.style()
        style.drawControl(QStyle.CE_ItemViewItem, option, painter, widget)
class NotEditDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        """ Return QComboBox with list of choices (either values or their associated keys if they exist).
        """
        return None
class EditDelegateQt(QStyledItemDelegate):
    """ Delegate for editing float values with arbitrary precision that may also be in scientific notation.
    Based on a QLineEdit rather than Qt's default QDoubleSpinBox that limits values to two decimal places
    and cannot handle scientific notation.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        """ Return a QLineEdit for arbitrary representation of a float value.
        """
        editor = QLineEdit(parent)
        value = index.model().data(index, Qt.DisplayRole)
        editor.setText(str(value))
        return editor

    def setModelData(self, editor, model, index):
        """ Cast the QLineEdit text to a float value, and update the model with this value.
        """
        try:
            value = editor.text()
            model.setData(index, value, Qt.EditRole)
        except:
            traceback.print_exc()
            pass  # If we can't cast the user's input to a float, don't do anything.        
class FloatEditDelegateQt(QStyledItemDelegate):
    """ Delegate for editing float values with arbitrary precision that may also be in scientific notation.
    Based on a QLineEdit rather than Qt's default QDoubleSpinBox that limits values to two decimal places
    and cannot handle scientific notation.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        """ Return a QLineEdit for arbitrary representation of a float value.
        """
        editor = QLineEdit(parent)
        value = index.model().data(index, Qt.DisplayRole)
        editor.setText(str(value))
        return editor

    def setModelData(self, editor, model, index):
        """ Cast the QLineEdit text to a float value, and update the model with this value.
        """
        try:
            value = float(editor.text())
            model.setData(index, value, Qt.EditRole)
        except:
            traceback.print_exc()
            pass  # If we can't cast the user's input to a float, don't do anything.
class ComboBoxCompleteDelegate(QStyledItemDelegate):
    """ Delegate for editing a list of choices via a combobox.
    The choices attribute is a list of either values or (key, value) tuples.
    In the first case, the str rep of the values are directly displayed in the combobox.
    In the latter case, the str rep of only the keys are displayed in the combobox, and the values can be any object.
        Although only the keys are displayed in the view, the model data is set to the actual values, not the keys.
    Upon selection, view will display the str rep of either the value itself or its key if it exists.

    For example:
        To select from some integers, set choices = [1, 3, 10, 100].
            Combobox entries will be '1', '3', '10' and '100'.
            Upon selection model data will be set to the selected integer value and view will show the str rep of this value.
        To select from two of your custom objects, set choices = [('A', MyObject()), ('B', MyObject())]
            Combobox entries will be 'A' and 'B'.
            Upon selection model data will be set to the selected MyObject instance and view will show its key (either 'A' or 'B')..
    """
    def __init__(self, choices=None,completer=None, parent=None):
        super().__init__(parent)
        self.completer=completer
        self.choices = choices if (choices is not None and type(choices) is list) else []

    def createEditor(self, parent, option, index):
        """ Return QComboBox with list of choices (either values or their associated keys if they exist).
        """
        try:
            editor = QComboBox(parent)
            editor.setEditable(True)
            editor.setCompleter(self.completer)
            value = index.model().data(index, Qt.DisplayRole)
            for i, choice in enumerate(self.choices):
                if (type(choice) is tuple) and (len(choice) == 2):
                    # choice is a (key, value) tuple.
                    key, val = choice
                    editor.addItem(str(key))  # key MUST be representable as a str.
                    if val == value:
                        editor.setCurrentIndex(i)
                else:
                    # choice is a value.
                    editor.addItem(str(choice))  # choice MUST be representable as a str.
                    if choice == value:
                        editor.setCurrentIndex(i)
            return editor
        except:
            return None

    def setModelData(self, editor, model, index):
        """ Set model data to current choice (if choice is a key, set data to its associated value).
        """
        try:
            choice = self.choices[editor.currentIndex()]
            if (type(choice) is tuple) and (len(choice) == 2):
                # choice is a (key, value) tuple.
                key, val = choice
                value = copy.deepcopy(val)  # Deepcopy of val in case it is a complex object.
            else:
                # choice is a value.
                value = choice
            model.setData(index, value, Qt.EditRole)
            index.model().dataChanged.emit(index, index)  # Tell model to update cell display.
        except:
            traceback.print_exc()
            pass

    def displayText(self, value, locale):
        """ Show str rep of current choice (or choice key if choice is a (key, value) tuple).
        """
        try:
            for choice in self.choices:
                if (type(choice) is tuple) and (len(choice) == 2):
                    # choice is a (key, value) tuple.
                    # Display the key, not the value.
                    key, val = choice
                    if val == value:
                        return str(key)
                else:
                    # choice is a value.
                    # Display it's str rep.
                    if choice == value:
                        return str(choice)
            # If value is not in our list of choices, show str rep of value.
            return str(value)
        except:
            traceback.print_exc()
            return ""
class DateTimeEditDelegateQt(QStyledItemDelegate):
    """ Delegate for editing datetime objects with a user specified format.
    The format attribute is a str that is passed to datetime.strftime() and datetime.strptime().
    """
    def __init__(self, format="%Y-%m-%d %H:%M:%S", parent=None):
        QStyledItemDelegate.__init__(self, parent)
        self.format = format

    def createEditor(self, parent, option, index):
        """ Return a QLineEdit for arbitrary representation of a date value in any format.
        """
        editor = QLineEdit(parent)
        date = index.model().data(index, Qt.ItemDataRole.EditRole)
        editor.setText(date.strftime(self.format))
        return editor

    def setModelData(self, editor, model, index):
        """ Only update the model data if the editor's current text conforms to the specified date format.
        """
        try:
            date = datetime.strptime(str(editor.text()), self.format)
            model.setData(index, date, Qt.ItemDataRole.EditRole)
        except:
            pass  # If the text does not conform to the date format, do nothing.

    def displayText(self, value, locale):
        """ Show the date in the specified format.
        """
        try:
            # logging.info([date,type(date)])
            if isinstance(value,datetime.datetime):
                return value.strftime(self.format)
            else:
                return value
        except:
            traceback.print_exc()
            return ""
class PushButtonDelegateQt(QStyledItemDelegate):
    """ Delegate for a clickable button in a model view.
    Calls the model's setData() method when clicked, wherein the button clicked action should be handled.
    """
    def __init__(self, text="", parent=None):
        QStyledItemDelegate.__init__(self, parent)
        self.text = text
        self._isMousePressed = False

    def createEditor(self, parent, option, index):
        """ Important, otherwise an editor is created if the user clicks in this cell.
        """
        return None

    def paint(self, painter, option, index):
        """ Draw button in cell.
        """
        opts = QStyleOptionButton()
        opts.state |= QStyle.State_Active
        opts.state |= QStyle.State_Enabled
            # When raised in PyQt5, white text cannot be seen on white background.
            # Should probably fix this by initializing form styled button, but for now I'll just sink it all the time.
        opts.state |= QStyle.State_Sunken
        opts.rect = option.rect
        opts.text = self.text
        QApplication.style().drawControl(QStyle.CE_PushButton, opts, painter)

    def editorEvent(self, event, model, option, index):
        """ Handle mouse events in cell.
        On left button release in this cell, call model's setData() method,
            wherein the button clicked action should be handled.
        Currently, the value supplied to setData() is the button text, but this is arbitrary.
        """
        if event.button() == Qt.LeftButton:
            if event.type() == QEvent.MouseButtonPress:
                if option.rect.contains(event.pos()):
                    self._isMousePressed = True
                    return True
            elif event.type() == QEvent.MouseButtonRelease:
                self._isMousePressed = False
                if option.rect.contains(event.pos()):
                    model.setData(index, self.text, Qt.EditRole)  # Model should handle button click action in its setData() method.
                    return True
        return False
class FileDialogDelegateQt(QStyledItemDelegate):
    """ Delegate that pops up a file dialog when double clicked.
    Sets the model data to the selected file name.
    """
    def __init__(self, parent=None):
        QStyledItemDelegate.__init__(self, parent)
    def setEditorData(self,editor, index):
        value = index.model().data(index)
        editor.setText(str(value))
    def setModelData(self,editor,model,index):
        value = editor.text()
        model.setData(index, value)
    def updateEditorGeometry(self,editor,option,index):
        editor.setGeometry(option.rect)
    def createEditor(self, parent, option, index):
        """ Instead of creating an editor, just popup a modal file dialog
        and set the model data to the selected file name, if any.
        """
        pathToFileName = ""
        pathToFileName, temp = QFileDialog.getOpenFileName(None, "Open")
        if pathToFileName!="":
            index.model().setData(index, pathToFileName, Qt.EditRole)
        # index.model().dataChanged.emit(index, index)  # Tell model to update cell display.
        editor =QtWidgets.QLineEdit(parent)
        return editor;
        # return None
    def paint(self,painter, option, index):
        #注意，此时index是logicIndex（model坐标），我们可以通过拖拽表头项交换列来测试
        #QStyle会根据option的属性进行绘制，我们也可以不使用QStyle的规则，完全自定义
        opt = option
        #去掉焦点 setFocusPolicy(Qt::NoFocus);
        # opt.state &= ~QStyle.State_HasFocus
        #参照源码实现了自己的initStyleOption
        # self.initMyStyleOption(opt, index)
        value = index.model().data(index)
        opt.text = value
        widget = opt.widget
        if widget:
            style = widget.style() 
        else:
            style= QApplication.style()
        style.drawControl(QStyle.CE_ItemViewItem, opt, painter, widget)
    def displayText(self, value, locale):
        """ Show file name without path.
        """
        pathToFileName = str(value)
        path, fileName = os.path.split(pathToFileName)
        return fileName
class PixmapDialogDelegate(QStyledItemDelegate):
    """ Delegate that pops up a file dialog when double clicked.
    Sets the model data to the selected file name.
    """
    def __init__(self, parent=None):
        QStyledItemDelegate.__init__(self, parent)
    def setEditorData(self,editor, index):
        value = index.model().data(index)
        if value!=None:
            editor.setPixmap(value)
    def setModelData(self,editor,model,index):
        value = editor.pixmap()
        if value!=None:
            model.setData(index, value)
    def updateEditorGeometry(self,editor,option,index):
        editor.setGeometry(option.rect)
    def createEditor(self, parent, option, index):
        """ Instead of creating an editor, just popup a modal file dialog
        and set the model data to the selected file name, if any.
        """
        pathToFileName = ""
        pathToFileName, temp =QtWidgets.QFileDialog.getOpenFileName(None, "Open")

        if pathToFileName!="":
            pix=QtGui.QPixmap(pathToFileName) 
            index.model().setData(index, pix, Qt.EditRole)
        # index.model().dataChanged.emit(index, index)  # Tell model to update cell display.
        editor =QtWidgets.QLabel(parent)
        pix=index.data(Qt.DisplayRole)
        if pix!=None:
            editor.setPixmap(pix)
        return editor;
        # return None
    def paint(self,painter, option, index): 
        data=index.data(Qt.DisplayRole)
        # print(data,dir(data),type(data))
        if isinstance(data, QtGui.QPixmap):#data is QPixmap:
            painter.drawPixmap(option.rect,data);
        super().paint(painter,option,index);        
    # def paint(self,painter, option, index):
    #     #注意，此时index是logicIndex（model坐标），我们可以通过拖拽表头项交换列来测试
    #     #QStyle会根据option的属性进行绘制，我们也可以不使用QStyle的规则，完全自定义
    #     opt = option
    #     #去掉焦点 setFocusPolicy(Qt::NoFocus);
    #     # opt.state &= ~QStyle.State_HasFocus
    #     #参照源码实现了自己的initStyleOption
    #     # self.initMyStyleOption(opt, index)
    #     value = index.model().data(index)
    #     opt.text = value
    #     widget = opt.widget
    #     if widget:
    #         style = widget.style() 
    #     else:
    #         style= QApplication.style()
    #     style.drawControl(QStyle.CE_ItemViewItem, opt, painter, widget)
    # def displayText(self, value, locale):
    #     """ Show file name without path.
    #     """
    #     pathToFileName = str(value)
    #     path, fileName = os.path.split(pathToFileName)
    #     return fileName
class PixmapDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
            QStyledItemDelegate.__init__(self, parent)   
    def paint(self,painter, option, index): 
        data=index.data(Qt.DisplayRole)
        # logging.info("here paint")
        # print(data,dir(data),type(data))
        if isinstance(data,QtGui.QPixmap):#data is QPixmap:
            painter.drawPixmap(option.rect,data);
        super().paint(painter,option,index);
class SpreadSheetDelegate(QStyledItemDelegate):
    def __init__(self, parent = None) -> None:
        super().__init__(parent)
        print("SpreadSheetDelegate __init__")

    def create_editor(self, parent: QtWidgets.QWidget,
                      option:QtWidgets.QStyleOptionViewItem,
                      index:QtCore.QModelIndex):
        print("here")
        if index.column() == 1:
            editor = QDateTimeEdit(parent)
            editor.setDisplayFormat("dd/M/yyyy")
            editor.setCalendarPopup(True)
            return editor

        editor = QLineEdit(parent)

        # create a completer with the strings in the column as model
        allStrings = QStringListModel()
        print(index.model())
        for i in range(1, index.model().rowCount()):
            strItem = str(index.model().data(index.sibling(i, index.column()), Qt.EditRole))

            if not allStrings.contains(strItem):
                allStrings.append(strItem)

        autoComplete = QCompleter(allStrings)
        editor.setCompleter(autoComplete)
        editor.editingFinished.connect(SpreadSheetDelegate.commit_and_close_editor)
        return editor

    @QtCore.pyqtSlot()
    def commit_and_close_editor(self) -> None:
        editor = self.sender()
        self.commitData.emit(editor)
        self.closeEditor.emit(editor)

    def set_editor_data(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex) -> None:
        print("set_editor_data")
        edit = QLineEdit(editor)
        if edit:
            edit.setText(str(index.model().data(index, Qt.EditRole)))
            return

        dateEditor = QDateTimeEdit(editor)
        if dateEditor:
            dateEditor.setDate(
                QDate.fromString(
                    str(index.model().data(index, Qt.EditRole)), "d/M/yyyy"))

    def set_model_data(self, editor:QtWidgets.QWidget,
                       model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex) -> None:
        edit = QLineEdit(editor)
        if edit:
            model.setData(index, edit.text())
            return

        dateEditor = QDateTimeEdit(editor)
        if dateEditor:
            model.setData(index, dateEditor.date().toString("dd/M/yyyy"))        

